/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.bruce.pickerview;

import com.bruce.pickerview.utils.AttrUtils;
import com.bruce.pickerview.utils.PickUtils;

import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.ScrollHelper;
import ohos.agp.components.VelocityDetector;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.utils.*;

import ohos.app.Context;

import ohos.eventhandler.EventHandler;
import ohos.eventhandler.EventRunner;
import ohos.multimodalinput.event.TouchEvent;

import java.math.BigDecimal;

import java.text.DecimalFormat;

import java.util.List;

/**
 * LoopView滚轮实现类
 */
public class LoopView extends Component implements
        Component.DrawTask,
        Component.EstimateSizeListener,
        Component.TouchEventListener {
    private static final int MAX_ANGLE = 90;
    private static final float INFLEXION = 0.35f;
    private static final float SCROLL_FRICTION = 0.015f;
    private static final float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
    private int mVisibleItemCount;
    private int mDrawnItemCount;
    private int mHalfDrawnItemCount;
    private int mItemSpace;
    private int mTextMaxWidth;
    private int mTextMaxHeight;
    private int wheelCenterY;
    private int drawnCenterX, drawnCenterY;
    private int mHalfWheelHeight;
    private int mItemHeight, mHalfItemHeight;
    private int minFlingY, maxFlingY;
    private boolean mCanLoop;
    private int mTextSize;
    private int mCenterLineColor;
    private int mCenterTextColor;
    private int mTopBottomTextColor;
    private boolean isClick;
    private int lastPointY;
    private int downPointY;
    private int scrollOffsetY;
    private boolean isForceFinishScroll;
    private int mInitPosition;
    private boolean mLayoutAlready = false;
    private final Matrix matrixRotate = new Matrix();
    private final Rect rectDrawn = new Rect();
    private final Rect rectCurrentItem = new Rect();
    private final RectFloat rectFloatCurrentItem = new RectFloat();
    private Paint paint;
    private ScrollHelper scrollHelper;
    private VelocityDetector tracker;
    private final EventHandler handler = new EventHandler(EventRunner.getMainEventRunner());
    private List<String> mDataList;
    private LoopScrollListener loopListener;

    /**
     * 构造函数
     *
     * @param context context
     * @param attrSet attrSet
     */
    public LoopView(Context context, AttrSet attrSet) {
        super(context, attrSet);
        initView(attrSet);
    }

    /**
     * 构造函数
     *
     * @param context   context
     * @param attrSet   attrSet
     * @param styleName styleName
     */
    public LoopView(Context context, AttrSet attrSet, String styleName) {
        super(context, attrSet, styleName);
        initView(attrSet);
    }

    /**
     * 构造函数
     *
     * @param context context
     * @param attrSet attrSet
     * @param resId   resId
     */
    public LoopView(Context context, AttrSet attrSet, int resId) {
        super(context, attrSet, resId);
        initView(attrSet);
    }

    private void initView(AttrSet attrSet) {
        if (attrSet != null) {
            mCenterLineColor = AttrUtils.getColor(attrSet, "lineColor", Color.getIntColor("#ffc5c5c5"));
            mCenterTextColor = AttrUtils.getColor(attrSet, "centerTextColor", Color.getIntColor("#ff313131"));
            mTopBottomTextColor = AttrUtils.getColor(attrSet, "topBottomTextColor", Color.getIntColor("#ffafafaf"));
            mCanLoop = AttrUtils.getBoolean(attrSet, "canLoop", true);
            mInitPosition = AttrUtils.getInteger(attrSet, "initPosition", -1);
            mTextSize = AttrUtils.getInteger(attrSet, "textSize", 20);
            mVisibleItemCount = AttrUtils.getInteger(attrSet, "visibleItemCount", 5);
        }
        mItemSpace = 16;
        updateVisibleItemCount();
        paint = new Paint();
        paint.setTextSize(PickUtils.vpToPx(getContext(), mTextSize));
        paint.setTextAlign(TextAlignment.CENTER);
        scrollHelper = new ScrollHelper();
        addDrawTask(this);
        setEstimateSizeListener(this);
        setTouchEventListener(this);
    }

    /**
     * 测量方法
     *
     * @param widthEstimateConfig  widthEstimateConfig
     * @param heightEstimateConfig heightEstimateConfig
     * @return boolean
     */
    @Override
    public boolean onEstimateSize(int widthEstimateConfig, int heightEstimateConfig) {
        final int modeWidth = EstimateSpec.getMode(widthEstimateConfig);
        final int modeHeight = EstimateSpec.getMode(heightEstimateConfig);
        final int sizeWidth = EstimateSpec.getSize(widthEstimateConfig);
        final int sizeHeight = EstimateSpec.getSize(heightEstimateConfig);
        int resultWidth = mTextMaxWidth;
        int resultHeight = mTextMaxHeight * mVisibleItemCount + mItemSpace * (mVisibleItemCount - 1);
        resultHeight = (int) (2 * sinDegree(MAX_ANGLE) / (Math.PI * MAX_ANGLE / 90f) * resultHeight);
        resultWidth += getPaddingLeft() + getPaddingRight();
        resultHeight += getPaddingTop() + getPaddingBottom();
        resultWidth = measureSize(modeWidth, sizeWidth, resultWidth);
        resultHeight = measureSize(modeHeight, sizeHeight, resultHeight);
        setEstimatedSize(resultWidth, resultHeight);
        return false;
    }

    /**
     * 绘制方法
     *
     * @param component component
     * @param canvas    canvas
     */
    @Override
    public void onDraw(Component component, Canvas canvas) {
        preDraw();
        drawContent(canvas);
    }

    /**
     * touch方法
     *
     * @param component component
     * @param event     event
     * @return boolean
     */
    @Override
    public boolean onTouchEvent(Component component, TouchEvent event) {
        if (isEnabled()) {
            int touchSlop = 8;
            int minimumVelocity = 50;
            int maximumVelocity = 8000;
            switch (event.getAction()) {
                case TouchEvent.PRIMARY_POINT_DOWN:
                    touchDown(event);
                    break;
                case TouchEvent.POINT_MOVE:
                    if (Math.abs(BigDecimal.valueOf(downPointY)
                            .subtract(BigDecimal.valueOf(event.getPointerScreenPosition(0).getY())).floatValue()) < touchSlop
                            && computeDistanceToEndPoint(scrollHelper.getCurrValue(AXIS_Y) % mItemHeight) > 0) {
                        isClick = true;
                        break;
                    }
                    isClick = false;
                    tracker.addEvent(event);
                    int move = BigDecimal.valueOf(event.getPointerScreenPosition(0).getY())
                            .subtract(BigDecimal.valueOf(lastPointY)).intValue();
                    if (Math.abs(move) < 1) {
                        break;
                    }
                    scrollOffsetY += move;
                    lastPointY = (int) event.getPointerScreenPosition(0).getY();
                    invalidate();
                    break;
                case TouchEvent.PRIMARY_POINT_UP:
                    if (isClick) {
                        break;
                    }
                    touchUp(event, maximumVelocity, minimumVelocity);
                    break;
                case TouchEvent.CANCEL:
                    if (tracker != null) {
                        tracker.clear();
                        tracker = null;
                    }
                    break;
            }
        }
        return true;
    }

    private void touchDown(TouchEvent event) {
        if (tracker == null) {
            tracker = VelocityDetector.obtainInstance();
        } else {
            tracker.clear();
        }
        tracker.addEvent(event);
        if (!scrollHelper.isFinished()) {
            scrollHelper.abortAnimation();
            isForceFinishScroll = true;
        }
        downPointY = lastPointY = (int) event.getPointerScreenPosition(0).getY();
    }

    private void touchUp(TouchEvent event, int maximumVelocity, int minimumVelocity) {
        if (tracker != null) {
            tracker.addEvent(event);
            tracker.calculateCurrentVelocity(600, maximumVelocity, maximumVelocity);
            isForceFinishScroll = false;
            int velocity = (int) tracker.getVerticalVelocity();
            if (Math.abs(velocity) > minimumVelocity) {
                double finalY = getSplineFlingDistance(velocity, scrollOffsetY, minFlingY, maxFlingY);
                int dxY = (int) (finalY - scrollOffsetY);
                scrollHelper.startScroll(0, scrollOffsetY, 0, dxY + computeDistanceToEndPoint((int) (finalY % mItemHeight)));
            } else {
                scrollHelper.startScroll(0, scrollOffsetY, 0, computeDistanceToEndPoint(scrollOffsetY % mItemHeight));
            }
            if (!mCanLoop) {
                if (scrollHelper.getFlingDistanceY(0) <= maxFlingY) {
                    scrollHelper.getFlingDistanceY(0);
                }
            }
            handler.postSyncTask(runnable);
            tracker.clear();
            tracker = null;
        }
    }

    private void updateVisibleItemCount() {
        if (mVisibleItemCount < 2) {
            throw new ArithmeticException("Wheel's visible item count can not be less than 2!");
        }
        if (mVisibleItemCount % 2 == 0) {
            mVisibleItemCount += 1;
        }
        mDrawnItemCount = mVisibleItemCount + 2;
        mHalfDrawnItemCount = mDrawnItemCount / 2;
    }

    private void initData() {
        if (mDataList == null) {
            throw new IllegalArgumentException("data list must not be null!");
        }
        measureTextWidthHeight();
        if (mInitPosition == -1) {
            if (mCanLoop) {
                mInitPosition = (mDataList.size() + 1) / 2;
            } else {
                mInitPosition = 0;
            }
        }
        invalidate();
    }

    private void measureTextWidthHeight() {
        mTextMaxWidth = mTextMaxHeight = 0;
        for (String text : mDataList) {
            int width = (int) paint.measureText(text);
            mTextMaxWidth = Math.max(mTextMaxWidth, width);
        }
        final Paint.FontMetrics metrics = paint.getFontMetrics();
        mTextMaxHeight = BigDecimal.valueOf(metrics.bottom)
                .subtract(BigDecimal.valueOf(metrics.top)).intValue();
    }

    private int measureSize(int mode, int sizeExpect, int sizeActual) {
        int realSize;
        if (mode == EstimateSpec.PRECISE) {
            realSize = sizeExpect;
        } else {
            realSize = sizeActual;
            if (mode == EstimateSpec.NOT_EXCEED) {
                realSize = Math.min(realSize, sizeExpect);
            }
        }
        return realSize;
    }

    private void preDraw() {
        if (mLayoutAlready) {
            return;
        }
        rectDrawn.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
        int wheelCenterX = rectDrawn.getCenterX();
        wheelCenterY = rectDrawn.getCenterY();
        drawnCenterX = wheelCenterX;
        BigDecimal cent = BigDecimal.valueOf(paint.ascent()).add(BigDecimal.valueOf(paint.descent()));
        BigDecimal half = cent.divide(BigDecimal.valueOf(2), BigDecimal.ROUND_HALF_UP);
        drawnCenterY = BigDecimal.valueOf(wheelCenterY).subtract(half).intValue();
        mHalfWheelHeight = rectDrawn.getHeight() / 2;
        mItemHeight = rectDrawn.getHeight() / mVisibleItemCount;
        mHalfItemHeight = mItemHeight / 2;
        computeFlingLimitY();
        computeCurrentItemRect();
        mLayoutAlready = true;
    }

    private void drawContent(Canvas canvas) {
        if (mItemHeight - mHalfDrawnItemCount <= 0) {
            return;
        }
        int drawnDataStartPos = -scrollOffsetY / mItemHeight - mHalfDrawnItemCount;
        for (int drawnDataPos = drawnDataStartPos + mInitPosition, drawnOffsetPos = -mHalfDrawnItemCount; drawnDataPos < drawnDataStartPos + mInitPosition + mDrawnItemCount; drawnDataPos++, drawnOffsetPos++) {
            String data = "";
            if (mCanLoop) {
                final int itemCount = mDataList.size();
                int actualPos = drawnDataPos % itemCount;
                actualPos = actualPos < 0 ? (actualPos + itemCount) : actualPos;
                data = mDataList.get(actualPos);
            } else {
                if (isPosInRang(drawnDataPos)) {
                    data = mDataList.get(drawnDataPos);
                }
            }
            paint.setColor(new Color(mTopBottomTextColor));
            paint.setStyle(Paint.Style.FILL_STYLE);
            int mDrawnItemCenterY = drawnCenterY + (drawnOffsetPos * mItemHeight) + scrollOffsetY % mItemHeight;
            float distanceToCenter;
            float ratio = (drawnCenterY - Math.abs(drawnCenterY - mDrawnItemCenterY) -
                    rectDrawn.top) * 1.0F / (drawnCenterY - rectDrawn.top);
            int unit = 0;
            if (mDrawnItemCenterY > drawnCenterY) {
                unit = 1;
            } else {
                if (mDrawnItemCenterY < drawnCenterY) {
                    unit = -1;
                }
            }
            BigDecimal temp1 = BigDecimal.valueOf(1).subtract(BigDecimal.valueOf(ratio));
            BigDecimal temp2 = temp1.multiply(BigDecimal.valueOf(MAX_ANGLE)).multiply(BigDecimal.valueOf(unit));
            float degree = clamp(-temp2.floatValue());
            distanceToCenter = computeYCoordinateAtAngle(degree);
            int alpha = (int) ((drawnCenterY - Math.abs(drawnCenterY - mDrawnItemCenterY)) * 1.0F / drawnCenterY * 255);
            alpha = Math.max(alpha, 0);
            float alphaF = BigDecimal.valueOf((float) alpha / 255).setScale(1, BigDecimal.ROUND_HALF_UP).floatValue();
            paint.setAlpha(alphaF);
            float drawnCenterYTemp = BigDecimal.valueOf(drawnCenterY)
                    .subtract(BigDecimal.valueOf(distanceToCenter)).floatValue();
            drawText(canvas, data, drawnCenterYTemp);
        }
    }

    private void drawText(Canvas canvas, String data, float centerY) {
        if (mCenterTextColor != -1) {
            canvas.save();
            canvas.concat(matrixRotate);
            rectFloatCurrentItem.bottom = rectCurrentItem.bottom;
            rectFloatCurrentItem.top = rectCurrentItem.top;
            rectFloatCurrentItem.right = rectCurrentItem.right;
            rectFloatCurrentItem.left = rectCurrentItem.left;
            canvas.clipRect(rectFloatCurrentItem, Canvas.ClipOp.DIFFERENCE);
            canvas.drawText(paint, data, drawnCenterX, centerY);
            canvas.restore();
            paint.setColor(new Color(mCenterTextColor));
            canvas.save();
            canvas.concat(matrixRotate);
            canvas.clipRect(rectFloatCurrentItem, Canvas.ClipOp.INTERSECT);
            canvas.drawText(paint, data, drawnCenterX, centerY);
            canvas.restore();
            canvas.save();
            paint.setColor(new Color(mCenterLineColor));
            float topY = BigDecimal.valueOf(rectFloatCurrentItem.top).subtract(BigDecimal.valueOf(10)).floatValue();
            float bottomY = BigDecimal.valueOf(rectFloatCurrentItem.bottom).add(BigDecimal.valueOf(10)).floatValue();
            canvas.drawLine(rectFloatCurrentItem.left, topY, rectFloatCurrentItem.right, topY, paint);
            canvas.drawLine(rectFloatCurrentItem.left, bottomY, rectFloatCurrentItem.right, bottomY, paint);
            canvas.restore();
        } else {
            canvas.save();
            canvas.clipRect(new RectFloat(rectDrawn));
            canvas.concat(matrixRotate);
            canvas.drawText(paint, data, drawnCenterX, centerY);
            canvas.restore();
        }
    }

    private void computeFlingLimitY() {
        int currentItemOffset = mInitPosition * mItemHeight;
        minFlingY = mCanLoop ? Integer.MIN_VALUE
                : -mItemHeight * (mDataList.size() - 1) + currentItemOffset;
        maxFlingY = mCanLoop ? Integer.MAX_VALUE : currentItemOffset;
    }

    private void computeCurrentItemRect() {
        rectCurrentItem.set(rectDrawn.left, wheelCenterY - mHalfItemHeight, rectDrawn.right, wheelCenterY + mHalfItemHeight);
    }

    private float computeYCoordinateAtAngle(float degree) {
        return sinDegree(degree) / sinDegree(MAX_ANGLE) * mHalfWheelHeight;
    }

    private float sinDegree(float degree) {
        return (float) Math.sin(Math.toRadians(degree));
    }

    private boolean isPosInRang(int position) {
        return position >= 0 && position < mDataList.size();
    }

    private float clamp(float value) {
        if (value < (float) -90) {
            return (float) -90;
        }
        return Math.min(value, 90);
    }

    private int computeDistanceToEndPoint(int remainder) {
        if (Math.abs(remainder) > mHalfItemHeight) {
            if (scrollOffsetY < 0) {
                return -mItemHeight - remainder;
            } else {
                return mItemHeight - remainder;
            }
        } else {
            return -remainder;
        }
    }

    private double getSplineFlingDistance(float velocityY, int startY, int mMinY, int mMaxY) {
        final double tempL = getSplineDeceleration(velocityY);
        float velocity = (float) Math.hypot((float) 0, velocityY);
        float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
        final double decelMinusOne = BigDecimal.valueOf(DECELERATION_RATE).subtract(BigDecimal.valueOf(1.0)).doubleValue();
        double totalDistance = SCROLL_FRICTION * computeDeceleration() * Math.exp(DECELERATION_RATE / decelMinusOne * tempL);
        int mFinalY = startY + (int) Math.round(totalDistance * coeffY);
        mFinalY = Math.min(mFinalY, mMaxY);
        mFinalY = Math.max(mFinalY, mMinY);
        return mFinalY;
    }

    private double getSplineDeceleration(float velocity) {
        return Math.log(INFLEXION * Math.abs(velocity) / (SCROLL_FRICTION * computeDeceleration()));
    }

    private float computeDeceleration() {
        BigDecimal mPpi = BigDecimal.valueOf(3).multiply(BigDecimal.valueOf(160.0f));
        return BigDecimal.valueOf(9.80665f)
                .multiply(BigDecimal.valueOf(39.37f))
                .multiply(mPpi)
                .multiply(BigDecimal.valueOf(0.84)).floatValue();
    }

    protected int calculatePosition(int itemCount) {
        float num = (float) -scrollOffsetY / mItemHeight;
        DecimalFormat df = new DecimalFormat("0.0");
        String result = df.format(num);
        int wholeValue = (int) Math.rint(Double.parseDouble(result));
        return (wholeValue + mInitPosition) % itemCount;
    }

    private final Runnable runnable = new Runnable() {
        @Override
        public void run() {
            if (mDataList == null) {
                return;
            }
            final int itemCount = mDataList.size();
            if (itemCount == 0) {
                return;
            }
            if (scrollHelper.isFinished() && !isForceFinishScroll) {
                if (mItemHeight == 0) {
                    return;
                }
                scrollOffsetY = scrollHelper.getCurrValue(AXIS_Y);
                int position = calculatePosition(itemCount);
                position = position < 0 ? position + itemCount : position;
                if (loopListener != null) {
                    loopListener.onItemSelect(position);
                }
            }
            if (!scrollHelper.isFinished()) {
                scrollHelper.updateScroll();
                scrollOffsetY = scrollHelper.getCurrValue(AXIS_Y);
                handler.postTask(this, 16, EventHandler.Priority.HIGH);
                invalidate();

            }
        }
    };

    /**
     * 设置数据源
     *
     * @param list 数据源
     */
    public final void setDataList(List<String> list) {
        this.mDataList = list;
        initData();
    }

    /**
     * 设置是否可以循环滚动
     *
     * @param canLoop 循环
     */
    public void setCanLoop(boolean canLoop) {
        this.mCanLoop = canLoop;
        invalidate();
    }

    /**
     * 设置字体大小
     *
     * @param size 大小
     */
    public final void setTextSize(int size) {
        if (size > 0) {
            mTextSize = PickUtils.vpToPx(mContext, size);
        }
    }

    /**
     * 设置初始值
     *
     * @param position 初始值
     */
    public void setInitPosition(int position) {
        mInitPosition = position;
        invalidate();
    }

    /**
     * 设置滚动监听
     *
     * @param listener 监听
     */
    public void setLoopListener(LoopScrollListener listener) {
        this.loopListener = listener;
    }

    /**
     * 滚轮Item选择监听类
     */
    public interface LoopScrollListener {
        /**
         * Item选择
         *
         * @param item item
         */
        void onItemSelect(int item);
    }
}